package cn.demoncat.util.pdf;

import cn.demoncat.util.exception.IoRuntimeException;
import cn.demoncat.util.io.CloseUtil;
import cn.demoncat.util.io.IoUtil;
import cn.demoncat.util.lang.ParamEmptyUtil;
import cn.demoncat.util.lang.constant.CharsetConstant;
import cn.demoncat.util.pdf.itextpdf.entity.*;
import cn.demoncat.util.pdf.itextpdf.event.PageMarker;
import cn.demoncat.util.pdf.itextpdf.event.WaterMarker;
import cn.demoncat.util.sec.SecurityUtil;
import cn.demoncat.util.web.ResourceUtil;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.attach.impl.OutlineHandler;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.image.ImageData;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.signatures.*;
import com.itextpdf.signatures.PdfSignatureAppearance.RenderingMode;
import com.itextpdf.signatures.PdfSigner.CryptoStandard;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import wiki.xsx.core.pdf.doc.XEasyPdfDocument;
import wiki.xsx.core.pdf.doc.XEasyPdfDocumentExtractor;
import wiki.xsx.core.pdf.handler.XEasyPdfHandler;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * PDF工具
 *
 * itextpdf：convert(html2pdf)、sign、verify
 *
 * easypdf：getText、build
 * 
 * @author 延晓磊
 *
 * @since 2020年6月4日
 */
public class PdfUtil {

	/**
	 * 文件扩展名
	 */
	public static final String PDF_EXTENSION = ".pdf";
	
	/**
	 * 字体库
	 * 
	 * 阿里巴巴普惠体 AlibabaPuHuiTiR
	 */
	public static final String FONT_PATH = ResourceUtil.getPath("fonts/Alibaba-PuHuiTi-Regular.otf");
	/**
	 * 字体选择器（输出中文内容）
	 */
	public static FontProvider FONT_PROVIDER;
	
	static {
		// 初始化字体
		FONT_PROVIDER = new FontProvider();
		FONT_PROVIDER.addFont(FONT_PATH);
	}
	
	
	/**
	 * 转换HTML为PDF，添加数字签名
	 * 
	 * @param convertParam	生成参数：不需要out流
	 * @param signParam		签名参数：不需要src流
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月6日
	 */
	public static void convertAndSign(PdfParam convertParam, PdfSignParam signParam) {
		// 生成HTML为PDF，保存到缓存 > 读取缓存并签名，输出到目的地
		ByteArrayOutputStream temp = new ByteArrayOutputStream();
		try {
			// 生成PDF
			signParam.pageCount(convert(convertParam.out(temp)));
			// 数字签名
			sign(signParam.src(new ByteArrayInputStream(temp.toByteArray())));
		} finally {
			CloseUtil.close(signParam.getOut());
		}
	}
	
	/**
	 * 转换HTML为PDF
	 * 
	 * 注：输出A4纸PDF，尺寸为595x842；自动边框偏移、段落换行；尺寸设置过宽的页面右部分会贴边隐藏
	 * 
	 * @param param
	 * 
	 * @return 总页数
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月5日
	 */
	public static int convert(PdfParam param) {
		// 创建输出器，内容压缩
		PdfWriter writer = new PdfWriter(param.getOut(), new WriterProperties().setFullCompressionMode(Boolean.TRUE));
		// 创建文档
		PdfDocument doc = new PdfDocument(writer);
		// 设置页面（更改doc页面尺寸，将关联更改A4常量）
        doc.setDefaultPageSize(PageSize.A4);
        // 字体：处理中文页码、水印，每个文档要独立创建，否则会报错"Pdf indirect object belongs to other PDF document"
        PdfFont font = null;
        // 添加页码
		if (param.getPager() == 2) {
			font = getFont();
		}
		doc.addEventHandler(PdfDocumentEvent.END_PAGE, new PageMarker(font,param));
        // 添加水印
        if (StringUtils.isNotBlank(param.getWater())) {
        	if (font == null) {
        		font = getFont();
			}
        	doc.addEventHandler(PdfDocumentEvent.END_PAGE, new WaterMarker(font, param));
		}
        // 输出
    	HtmlConverter.convertToPdf(param.getHtml(), doc, getConfig(param.getBaseUrl()));
    	// 总页数
    	return param.getPageCount();
	}
	
	/**
	 * 数字签名-电子签章
	 * 
	 * @param param
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月6日
	 */
	public static void sign(PdfSignParam param) {
		// 参数校验
		ParamEmptyUtil.check(CollectionUtils.isNotEmpty(param.getAreas()));
		// 填充总页数
		fillPageCount(param);
		// 印章图片
		ImageData graphic = ImageDataFactory.create(param.getImg());
		// 摘要算法
        IExternalDigest digest = new BouncyCastleDigest();
        // 签名算法：私钥，哈希算法，算法提供者
        IExternalSignature signature = new PrivateKeySignature(param.getPk(), param.getAlgorithm(), SecurityUtil.PROVIDER_NAME);
        PdfReader reader = null;
    	try {
    		// 读取源PDF：读取为byte[]，input流未关闭
			reader = new PdfReader(param.getSrc());
			// 签章
			if (param.getAreas().size() == 1) {
				// 单次签章：直接输出
				sign(reader, param.getOut(), param, param.getAreas().get(0), graphic, digest, signature, true);
			}else {
				// 多次签章：多次输出
		        ByteArrayOutputStream out = null;
		        int times = 1;
				for (PdfSignArea area : param.getAreas()) {
					if (times > 1) {
						// 读取已签名的临时PDF
						reader = new PdfReader(new ByteArrayInputStream(out.toByteArray()));
					}
					// 签名并输出到缓存
					out = new ByteArrayOutputStream();
					sign(reader, out, param, area, graphic, digest, signature, times == param.getAreas().size());
					times ++;
				}
				// 读取原数据 > 输出到缓存 > 读取缓存 > 输出到缓存 ... 读取并输出缓存到目标
				assert out != null;
				IoUtil.write(out.toByteArray(), param.getOut());
			}
		} catch (Exception e) {
			throw new IoRuntimeException("PDF数字签名失败", e);
		}finally{
			CloseUtil.closeAll(reader, param.getSrc(), param.getOut());
		}
	}
	
	/**
	 * 签名验证
	 * 
	 * @param src
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月6日
	 */
	public static PdfSignVerify verify(InputStream src) {
		PdfReader reader = null;
		PdfDocument doc = null;
		try {
			// 读取PDF
			reader = new PdfReader(src);
			doc = new PdfDocument(reader);
			// 创建签名工具
			SignatureUtil signatureUtil = new SignatureUtil(doc);
			// 遍历所有签名
			PdfSignVerify verify = new PdfSignVerify();
			PdfPKCS7 signatureData;
			boolean signatureVerify;
			for (String name : signatureUtil.getSignatureNames()) {
			    // 获取签名数据
				signatureData = signatureUtil.readSignatureData(name, SecurityUtil.PROVIDER_NAME);
			    // 验证签名
				signatureVerify = signatureData.verifySignatureIntegrityAndAuthenticity();
				// 包装
				verify.getDatas().add(new PdfSignData(signatureData, signatureVerify));
				if (!signatureVerify) {
					// 1个false就false
					verify.setVerifie(false);
				}
			}
			return verify;
		} catch (Exception e) {
			throw new IoRuntimeException("PDF验签失败", e);
		} finally {
			CloseUtil.closeAll(reader, doc, src);
		}
	}

	/**
	 * PDF文字提取
	 *
	 * @param in	.pdf
	 *
	 * @return  文字：因为PDF是图像，所以提取时受字体等影响，可能出现乱码
	 *
	 * @author 延晓磊
	 * @since 2021年07月15日
	 */
	public static List<String> getText(InputStream in){
		XEasyPdfDocument document = null;
		try {
			// 读取文档
			document = XEasyPdfHandler.Document.load(in);
			// 获取文档图像器
			XEasyPdfDocumentExtractor extractor = document.extractor();
			// 定义待接收文本列表
			List<String> dataList = new ArrayList<>();
			// 提取文本
			extractor.extractText(dataList);
			// 关闭文档
			document.close();
			// 截取空白
			return dataList.parallelStream().map(StringUtils::trimToEmpty).collect(Collectors.toList());
		} catch (Exception e) {
			throw new IoRuntimeException("PDF文字提取失败", e);
		} finally {
			CloseUtil.closeAll(in, document);
		}
	}

	/**
	 * 构建PDF
	 *
	 * 示例：PdfTest.build
	 * 参考：https://dromara.gitee.io/x-easypdf
	 *
	 * @param out	输出目标
	 * @param draw	创建页面，绘制文档
	 *
	 * @author 延晓磊
	 * @since 2022年07月13日
	 */
	public static void build(OutputStream out, Consumer<XEasyPdfDocument> draw){
		// 创建文档
		XEasyPdfDocument document = XEasyPdfHandler.Document.build();
		// 设置字体
		document.setFontPath(FONT_PATH);
		try {
			// 绘制文档
		 	draw.accept(document);
		 	// 输出文档
			document.save(out);
		}catch (Exception e){
		   	throw new IoRuntimeException("构建PDF失败", e);
		}finally {
			// 关闭文档
			CloseUtil.closeAll(document, out);
		}
	}





	/**
	 * 数字签名
	 * 
	 * @param reader	源数据
	 * @param out		输出流
	 * @param param		签名参数
	 * @param area		签名区
	 * @param graphic	印章图片
	 * @param digest	摘要算法
	 * @param signature	签名算法
	 * @param isLast	最后一次（设置认证等级）
	 * 
	 * @throws IOException
	 * @throws GeneralSecurityException
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月6日
	 */
	private static void sign(PdfReader reader, OutputStream out, PdfSignParam param, PdfSignArea area, ImageData graphic, IExternalDigest digest, IExternalSignature signature, boolean isLast) throws Exception {
		// 创建签名器
		PdfSigner signer =  new PdfSigner(reader, out, PdfSignParam.STAMP_PROPERTIES);
		// 区域默认值
		if (area.getPage() == null) {
			// 页码：默认尾页
			area.page(param.getPageCount());
		}
		// 图形宽高
		if (area.getWidth() == null) {
			// 图形宽高
			area.width(graphic.getWidth(), graphic.getHeight());
		}else if(area.getHeight() == null) {
			// 宽固定，高等比例
			area.setHeight(graphic.getHeight() / graphic.getWidth() * area.getWidth());
		}
		// 签名等级
		if (isLast) {
			signer.setCertificationLevel(param.getCertificationLevel());
		}
		// 签名
		sign(signer, param, area, graphic, digest, signature);
	}
	
	/**
	 * 数字签名
	 * 
	 * @param signer	签名器
	 * @param param		签名参数
	 * @param area		签名区
	 * @param graphic	印章图片
	 * @param digest	摘要算法
	 * @param signature	签名算法
	 * 
	 * @throws IOException
	 * @throws GeneralSecurityException
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月6日
	 */
	private static void sign(PdfSigner signer, PdfSignParam param, PdfSignArea area, ImageData graphic, IExternalDigest digest, IExternalSignature signature) throws Exception {
		// 印章样式
		signer.getSignatureAppearance()
				// 页码
				.setPageNumber(area.getPage())
				// 位置
				.setPageRect(new Rectangle(area.getX(), area.getY(), area.getWidth(), area.getHeight()))
				// 图片
				.setSignatureGraphic(graphic)
	            // 模式：图表 
				.setRenderingMode(RenderingMode.GRAPHIC)
				// 签名理由
				.setReason(area.getReason())
				// 签名位置
				.setLocation(area.getLocation());
		
		// 执行签名：摘要算法，签名算法，证书链，证书吊销列表，证书状态协议客户关，签名大小预估，签名类型(电子签名)
		signer.signDetached(digest, signature, param.getChain(), null, null, null, 0, CryptoStandard.CADES);
	}
	
	/**
	 * 填充总页数
	 * 
	 * 说明：获取总页数，以便在尾页印章，同时保证不重复读取文件
	 * 
	 * @param param
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月10日
	 */
	private static void fillPageCount(PdfSignParam param) {
		// 页数未设置
		if (param.getPageCount() == null) {
			// 遍历印章
			for (PdfSignArea area : param.getAreas()) {
				// 需要尾页印章
				if (area.getPage() == null) {
					// 将InputStream读取到byte[]，方便重复使用
					byte[] bytes = IoUtil.readToBytes(param.getSrc());
					// 获取尾页号
					PdfDocument doc = null;
					PdfReader reader = null;
					try {
						reader = new PdfReader(new ByteArrayInputStream(bytes));
						doc = new PdfDocument(reader);
						param.setPageCount(doc.getNumberOfPages());
					} catch (IOException e) {
						throw new IoRuntimeException("PDF数字签名失败", e);
					}finally {
						CloseUtil.closeAll(reader, doc);
					}
					// 重置源文件
					param.setSrc(new ByteArrayInputStream(bytes));
				}
			}
		}
	}
	
	/**
	 * 创建字体
	 * 
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月4日
	 */
	private static PdfFont getFont() {
		try {
			// 每次创建文档，都要新建字体实例，否则会抛异常 PdfException: Pdf indirect object belongs to other PDF document. Copy object to current pdf document.
			return PdfFontFactory.createFont(FONT_PATH, PdfEncodings.IDENTITY_H, true);
		} catch (IOException e) {
			throw new IoRuntimeException("创建字体失败", e);
		}
	}
	
	/**
	 * 获取转换配置
	 * 
	 * @param baseUrl	HTML源码中的根路径（相对路径的css、img的根地址）
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年6月4日
	 */
	private static ConverterProperties getConfig(String baseUrl) {
		ConverterProperties properties = new ConverterProperties();
		// 处理中文内容
    	properties.setFontProvider(FONT_PROVIDER);
        properties.setOutlineHandler(OutlineHandler.createStandardHandler());
        properties.setCharset(CharsetConstant.UTF8);
        // HTML源码中的根路径（相对路径的css、img的根地址）
        properties.setBaseUri(baseUrl);
        return properties;
	}
	
}
